// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.dashboard.ui.journey

import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.graphics.RectF
import android.graphics.drawable.LayerDrawable
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.Spanned
import android.text.TextWatcher
import android.text.format.DateUtils
import android.text.style.ImageSpan
import android.util.Log
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.Menu
import android.view.MotionEvent.ACTION_UP
import android.view.View
import android.view.ViewGroup
import android.widget.PopupMenu
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import com.google.openlocationcode.OpenLocationCode
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.dashboard.DashboardViewModel
import xyz.apiote.bimba.czwek.dashboard.MainActivity
import xyz.apiote.bimba.czwek.data.exceptions.GeocodingFailedException
import xyz.apiote.bimba.czwek.data.traffic.Place
import xyz.apiote.bimba.czwek.databinding.FragmentJourneyBinding
import xyz.apiote.bimba.czwek.dpToPixelI
import xyz.apiote.bimba.czwek.journeys.JourneysActivity
import xyz.apiote.bimba.czwek.repo.JourneyParams
import xyz.apiote.bimba.czwek.repo.Position
import xyz.apiote.bimba.czwek.repo.TimeReference
import xyz.apiote.bimba.czwek.search.Query
import xyz.apiote.bimba.czwek.search.Query.Mode
import xyz.apiote.bimba.czwek.search.ui.results.ResultsActivity
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId

class JourneyFragment : Fragment(), LocationListener {
	companion object {
		const val PLACE_KEY = "PLACE"
	}

	private var _binding: FragmentJourneyBinding? = null
	private val binding get() = _binding!!

	private lateinit var dashboard: MainActivity
	private lateinit var viewModel: JourneyViewModel

	private val activityLauncher =
		registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
			val place = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) {
				it.data?.extras?.getParcelable(PLACE_KEY, Place::class.java)
			} else {
				@Suppress("DEPRECATION")
				it.data?.extras?.getParcelable(PLACE_KEY)
			}
			place?.let { dashboard.viewModel.set(viewModel.searchRequester!!, it) }
		}

	override fun onCreateView(
		inflater: LayoutInflater,
		container: ViewGroup?,
		savedInstanceState: Bundle?
	): View {
		viewModel = ViewModelProvider(this)[JourneyViewModel::class.java]

		_binding = FragmentJourneyBinding.inflate(inflater, container, false)
		val root: View = binding.root

		dashboard = activity as MainActivity

		dashboard.viewModel.textInputs[DashboardViewModel.ORIGIN_KEY] = binding.origin
		dashboard.viewModel.textInputs[DashboardViewModel.DEST_KEY] = binding.destination

		dashboard.viewModel.suggestions[DashboardViewModel.ORIGIN_KEY] = binding.originSuggestions
		dashboard.viewModel.suggestions[DashboardViewModel.DEST_KEY] = binding.destinationSuggestions

		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, windowInsets ->
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())

			binding.chipsParamsTime.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
					topMargin = insets.top + dpToPixelI(16f)
				}
			}

			binding.originInput.updatePadding(left = insets.left, right = insets.right)
			binding.originInput.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				topMargin = insets.top + dpToPixelI(16f)
				windowInsets.displayCutout?.safeInsetLeft?.let {
					leftMargin = it + dpToPixelI(16f)
				}
				windowInsets.displayCutout?.safeInsetRight?.let {
					rightMargin = it + dpToPixelI(16f)
				}
			}
			binding.originSuggestions.updatePadding(left = insets.left, right = insets.right)
			binding.originSuggestions.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				windowInsets.displayCutout?.safeInsetLeft?.let {
					leftMargin = it + dpToPixelI(16f)
				}
				windowInsets.displayCutout?.safeInsetRight?.let {
					rightMargin = it + dpToPixelI(16f)
				}
			}
			binding.destinationInput.updatePadding(left = insets.left, right = insets.right)
			binding.destinationInput.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				windowInsets.displayCutout?.safeInsetLeft?.let {
					leftMargin = it + dpToPixelI(16f)
				}
				windowInsets.displayCutout?.safeInsetRight?.let {
					rightMargin = it + dpToPixelI(16f)
				}
			}
			binding.destinationSuggestions.updatePadding(left = insets.left, right = insets.right)
			binding.destinationSuggestions.updateLayoutParams<ViewGroup.MarginLayoutParams> {
				windowInsets.displayCutout?.safeInsetLeft?.let {
					leftMargin = it + dpToPixelI(16f)
				}
				windowInsets.displayCutout?.safeInsetRight?.let {
					rightMargin = it + dpToPixelI(16f)
				}
			}
			windowInsets
		}

		dashboard.hideBadge()
		chipifyOrigin(dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.value)
		chipifyDestination(dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.value)

		initSuggestions(DashboardViewModel.ORIGIN_KEY)
		initSuggestions(DashboardViewModel.DEST_KEY)

		binding.goButton.isEnabled =
			dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.value != null && dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.value != null

		binding.goButton.setOnClickListener {
			val intent = JourneysActivity.getIntent(
				requireContext(),
				dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.value!!,
				dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.value!!,
				JourneyParams(
					dashboard.viewModel.timeReference,
					dashboard.viewModel.date?.let { epochMilli -> Instant.ofEpochMilli(epochMilli) }
						?.atZone(ZoneId.systemDefault())?.toLocalDate(),
					dashboard.viewModel.time,
					dashboard.viewModel.wheelchairAccessible,
					dashboard.viewModel.bicycle
				)
			)
			startActivity(intent)
		}

		dashboard.viewModel.data[DashboardViewModel.ORIGIN_KEY]!!.observe(viewLifecycleOwner) {
			chipifyOrigin(it)
		}

		dashboard.viewModel.data[DashboardViewModel.DEST_KEY]!!.observe(viewLifecycleOwner) {
			chipifyDestination(it)
		}

		binding.origin.setOnKeyListener { v, keyCode, event ->
			when (keyCode) {
				KeyEvent.KEYCODE_ENTER -> {
					if (event.action == KeyEvent.ACTION_UP) {
						searchText(DashboardViewModel.ORIGIN_KEY)
						true
					} else {
						false
					}
				}

				else -> false
			}
		}

		binding.destination.setOnKeyListener { v, keyCode, event ->
			when (keyCode) {
				KeyEvent.KEYCODE_ENTER -> {
					if (event.action == KeyEvent.ACTION_UP) {
						searchText(DashboardViewModel.DEST_KEY)
						true
					} else {
						false
					}
				}

				else -> false
			}
		}

		binding.origin.addTextChangedListener(object : TextWatcher {
			override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

			override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

			override fun afterTextChanged(s: Editable?) {
				afterTextChanged(DashboardViewModel.ORIGIN_KEY, s)
			}
		})

		binding.destination.addTextChangedListener(object : TextWatcher {
			override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

			override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}

			override fun afterTextChanged(s: Editable?) {
				afterTextChanged(DashboardViewModel.DEST_KEY, s)
			}
		})

		setTimeReference(dashboard.viewModel.timeReference.id())
		binding.chipTimeReference.setOnClickListener {
			val menu = PopupMenu(requireContext(), it)
			menu.menu.add(
				Menu.NONE,
				TimeReference.DEPART_AFTER.id(),
				TimeReference.DEPART_AFTER.ordinal,
				R.string.depart_after
			)
			menu.menu.add(
				Menu.NONE,
				TimeReference.ARRIVE_BY.id(),
				TimeReference.ARRIVE_BY.ordinal,
				R.string.arrive_by
			)
			menu.setOnMenuItemClickListener {
				setTimeReference(it.itemId)
				true
			}
			menu.show()
		}

		if (dashboard.viewModel.date != null) {
			binding.chipDate.text =
				DateUtils.formatDateTime(
					context,
					dashboard.viewModel.date!!,
					DateUtils.FORMAT_SHOW_DATE
				)
		} else {
			binding.chipDate.setText(R.string.today)
		}
		binding.chipDate.setOnClickListener { _ ->
			MaterialDatePicker.Builder.datePicker().setTitleText(R.string.title_select_date_journey)
				.setNegativeButtonText(R.string.clear_date_selection)
				.apply {
					if (dashboard.viewModel.date != null) {
						setSelection(dashboard.viewModel.date!!)
					}
				}
				.build()
				.apply {
					addOnNegativeButtonClickListener { _ ->
						dashboard.viewModel.date = null
						binding.chipDate.setText(R.string.today)
					}
					addOnPositiveButtonClickListener { t ->
						dashboard.viewModel.date = t
						binding.chipDate.text =
							DateUtils.formatDateTime(context, t, DateUtils.FORMAT_SHOW_DATE)
					}
				}.show((activity as MainActivity).supportFragmentManager, null)
		}


		if (dashboard.viewModel.date != null) {
			binding.chipTime.text =
				DateUtils.formatDateTime(
					context,
					dashboard.viewModel.time!!.atDate(LocalDate.now()).atZone(ZoneId.systemDefault())
						.toEpochSecond() * 1000,
					DateUtils.FORMAT_SHOW_TIME
				)
		} else {
			binding.chipTime.setText(R.string.now)
		}
		binding.chipTime.setOnClickListener { _ ->
			val picker = MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_journey)
				.setTimeFormat(TimeFormat.CLOCK_24H)
				.setNegativeButtonText(R.string.clear_date_selection)
				.apply {
					if (dashboard.viewModel.time != null) {
						setHour(dashboard.viewModel.time!!.hour)
						setMinute(dashboard.viewModel.time!!.minute)
					} else {
						setHour(LocalTime.now().hour)
						setMinute(LocalTime.now().minute)
					}
				}.build()

			picker.apply {
				addOnNegativeButtonClickListener { _ ->
					dashboard.viewModel.time = null
					binding.chipTime.setText(R.string.now)
				}
				addOnPositiveButtonClickListener { _ ->
					dashboard.viewModel.time = LocalTime.of(picker.hour, picker.minute)
					binding.chipTime.text =
						DateUtils.formatDateTime(
							context,
							dashboard.viewModel.time!!.atDate(LocalDate.now()).atZone(ZoneId.systemDefault())
								.toEpochSecond() * 1000,
							DateUtils.FORMAT_SHOW_TIME
						)
				}
			}.show((activity as MainActivity).supportFragmentManager, null)
		}

		binding.chipWheelchair.isChecked = dashboard.viewModel.wheelchairAccessible
		binding.chipWheelchair.setOnClickListener {
			dashboard.viewModel.wheelchairAccessible = binding.chipWheelchair.isChecked
		}

		binding.chipBike.isChecked = dashboard.viewModel.bicycle
		binding.chipBike.setOnClickListener {
			dashboard.viewModel.bicycle = binding.chipBike.isChecked
		}

		return root
	}

	private fun setTimeReference(option: Int) {
		when (option) {
			TimeReference.DEPART_AFTER.id() -> {
				binding.chipTimeReference.setText(R.string.depart_after)
				dashboard.viewModel.timeReference = TimeReference.DEPART_AFTER
			}

			TimeReference.ARRIVE_BY.id() -> {
				binding.chipTimeReference.setText(R.string.arrive_by)
				dashboard.viewModel.timeReference = TimeReference.ARRIVE_BY
			}
		}
	}

	private fun afterTextChanged(
		source: String,
		s: Editable?,
	) {
		val suggestions = dashboard.viewModel.suggestions[source]!!
		if (s.isNullOrBlank()) {
			dashboard.viewModel.spans[source] = ""
			initSuggestions(source)
			binding.goButton.isEnabled = false
			return
		}
		binding.goButton.isEnabled =
			s.toString().replace(
				dashboard.viewModel.spans[source]!!,
				""
			) == "" && isOtherClean(source)

		val q = Query(
			dashboard.viewModel.textInputs[source]!!.text.toString()
				.replace(dashboard.viewModel.spans[source]!!, "").trim()
		)
		try {
			q.parse(requireContext())
		} catch (_: GeocodingFailedException) {
		}

		Log.i("Query", "${q.mode}, ‘${q.raw}’")
		if (q.mode in arrayOf(Mode.POSITION, Mode.LOCATION_PLUS_CODE)) {
			val chip = layoutInflater.inflate(R.layout.chip_suggestion, suggestions, false) as Chip
			chip.text = q.toString()
			chip.isCheckable = false
			chip.isChipIconVisible = true
			chip.setChipIconResource(R.drawable.position)
			chip.setOnClickListener {
				if (q.mode == Mode.POSITION) {
					dashboard.viewModel.set(
						source,
						q.position!!
					)
					initSuggestions(source)
				} else {
					setChipProgress(chip)
					dashboard.viewModel.positionQueries[source] = q
					viewModel.hereChipRequester = source
					setHere(source)
				}
			}
			initSuggestions(source)
			suggestions.addView(chip)
		} else if (q.mode == Mode.NAME && q.raw != "") {
			val chip = layoutInflater.inflate(R.layout.chip_suggestion, suggestions, false) as Chip
			chip.text = q.toString()
			chip.isCheckable = false
			chip.isChipIconVisible = true
			chip.setChipIconResource(R.drawable.search)
			chip.setOnClickListener {
				searchText(source)
			}
			initSuggestions(source)
			suggestions.addView(chip)
		} else {
			initSuggestions(source)
		}
	}

	fun initSuggestions(source: String) {
		val suggestions = dashboard.viewModel.suggestions[source]!!
		suggestions.removeAllViews()
		val chip = layoutInflater.inflate(R.layout.chip_suggestion, suggestions, false) as Chip
		chip.setText(R.string.here)
		chip.isCheckable = false
		chip.isChipIconVisible = true
		chip.setChipIconResource(R.drawable.gps)
		chip.setOnClickListener {
			setChipProgress(chip)
			setHere(source)
		}
		suggestions.addView(chip)
	}

	private fun getSearchText(source: String): String =
		dashboard.viewModel.textInputs[source]!!.text.toString()
			.replace(dashboard.viewModel.spans[source]!!, "")

	private fun searchText(source: String) {
		viewModel.searchRequester = source
		val text = getSearchText(source)
		if (text.isBlank()) {
			return
		}
		activityLauncher.launch(
			ResultsActivity.getIntent(
				requireContext(),
				Query(text),
				true,
				"transitous",
				true
			)
		)
	}

	private fun isOriginClean(): Boolean = binding.origin.text.toString().let {
		it.replace(dashboard.viewModel.spans[DashboardViewModel.ORIGIN_KEY]!!, "") == "" && it != ""
	}

	private fun isDestinationClean(): Boolean = binding.destination.text.toString().let {
		it.replace(dashboard.viewModel.spans[DashboardViewModel.DEST_KEY]!!, "") == "" && it != ""
	}

	private fun isOtherClean(source: String): Boolean =
		if (source == DashboardViewModel.ORIGIN_KEY) {
			isDestinationClean()
		} else if (source == DashboardViewModel.DEST_KEY) {
			isOriginClean()
		} else {
			throw IllegalArgumentException("source must be one of ${DashboardViewModel.ORIGIN_KEY}, ${DashboardViewModel.DEST_KEY}. found $source")
		}

	private fun chipifyOrigin(place: Place?) {
		val source = DashboardViewModel.ORIGIN_KEY
		val otherSource = DashboardViewModel.otherSource(source)
		if (place != null) {
			chipify(place, source)
			dashboard.viewModel.spans[source] = place.getShortName()
			if (dashboard.viewModel.data[otherSource]!!.value != null && isDestinationClean()) {
				binding.goButton.isEnabled = true
			}
		} else {
			binding.origin.setText("")
			binding.goButton.isEnabled = false
		}
	}

	private fun chipifyDestination(place: Place?) {
		val source = DashboardViewModel.DEST_KEY
		val otherSource = DashboardViewModel.otherSource(source)
		if (place != null) {
			chipify(place, source)
			dashboard.viewModel.spans[source] = place.getShortName()
			if (dashboard.viewModel.data[otherSource]!!.value != null && isOriginClean()) {
				binding.goButton.isEnabled = true
			}
		} else {
			binding.destination.setText("")
			binding.goButton.isEnabled = false
		}
	}

	private fun chipify(place: Place, source: String) {
		val textView = dashboard.viewModel.textInputs[source]!!
		val text = place.getShortName()
		textView.setText(text)
		var chip: ChipDrawable? = ChipDrawable.createFromResource(requireContext(), R.xml.journey_chip)
		chip!!.text = text
		val ld = LayerDrawable(arrayOf(chip)).apply {
			setLayerInset(0, dpToPixelI(4f), 0, dpToPixelI(4f), 0)
			setBounds(0, 0, chip.intrinsicWidth + 24, chip.intrinsicHeight)
		}
		val span = ImageSpan(ld)
		textView.text?.setSpan(span, 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
		@SuppressLint("ClickableViewAccessibility")
		textView.setOnTouchListener { v, e ->
			v.performClick()
			if (chip != null) {
				val chipContentRect = RectF()
				val chipCloseRect = RectF()
				chip!!.getChipTouchBounds(chipContentRect)
				chip!!.getCloseIconTouchBounds(chipCloseRect)
				if (e.x > textView.totalPaddingLeft && e.x < textView.totalPaddingLeft + chipContentRect.right
					&& e.y > textView.totalPaddingTop && e.y < textView.totalPaddingTop + chipContentRect.bottom
				) {
					if (e.action == ACTION_UP) {
						// TODO popup
						Log.i("Touch", "content")
					}
					true
				} else if (e.x > textView.totalPaddingLeft + chipContentRect.right && e.x < textView.totalPaddingLeft + chipCloseRect.right
					&& e.y > textView.totalPaddingTop && e.y < textView.totalPaddingTop + chipCloseRect.bottom
				) {
					if (e.action == ACTION_UP) {
						dashboard.viewModel.unset(source)
						chip = null
					}
					true
				} else {
					false
				}
			} else {
				false
			}
		}

		initSuggestions(source)
	}

	private fun setHere(source: String) {
		viewModel.hereChipRequester = source
		if (dashboard.onGpsClicked(this)) {
			try {
				val locationManager =
					requireContext().getSystemService(Context.LOCATION_SERVICE) as LocationManager
				locationManager.requestLocationUpdates(
					LocationManager.GPS_PROVIDER, 1000 * 60 * 10, 100f, this
				)
				locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)
					?.let { onLocationChanged(it) }
			} catch (_: SecurityException) {
				Log.wtf(
					"locate",
					"this shouldn’t happen because we don’t run this without location permission"
				)
			}
		}
	}

	override fun onDestroyView() {
		super.onDestroyView()
		_binding = null
	}

	override fun onSaveInstanceState(outState: Bundle) {
		super.onSaveInstanceState(outState)
		outState.putString(DashboardViewModel.ORIGIN_KEY, getSearchText(DashboardViewModel.ORIGIN_KEY))
		outState.putString(DashboardViewModel.DEST_KEY, getSearchText(DashboardViewModel.DEST_KEY))
	}

	override fun onViewStateRestored(savedInstanceState: Bundle?) {
		super.onViewStateRestored(savedInstanceState)

		if (savedInstanceState != null) {
			arrayOf(DashboardViewModel.ORIGIN_KEY, DashboardViewModel.DEST_KEY).forEach { source ->
				val searchString = savedInstanceState.getString(source) ?: ""
				if (searchString != "") {
					binding.origin.setText(searchString)
				}
			}
		}
	}

	override fun onLocationChanged(location: Location) {
		viewModel.hereChipRequester?.let {
			val query = dashboard.viewModel.positionQueries[it]
			val position = if (query != null && query.mode == Mode.LOCATION_PLUS_CODE) {
				OpenLocationCode(query.raw).recover(location.latitude, location.longitude).decode().let {
					viewModel.loadingChip?.setChipIconResource(R.drawable.position)
					Position(it.centerLatitude, it.centerLatitude)
				}
			} else {
				viewModel.loadingChip?.setChipIconResource(R.drawable.gps_black)
				Position(location.latitude, location.longitude)
			}
			dashboard.viewModel.set(
				it,
				position
			)
			dashboard.viewModel.positionQueries[it] = null
			initSuggestions(it)
		}
		viewModel.hereChipRequester = null
	}

	private fun setChipProgress(chip: Chip) {
		viewModel.loadingChip = chip
		chip.chipIcon = CircularProgressDrawable(requireContext()).apply {
			setStyle(CircularProgressDrawable.DEFAULT)
			start()
		}
	}
}